namespace NAct
{
	using System;
	using System.Collections.Generic;
	using System.Collections.ObjectModel;
	using System.IO;
	using System.Linq;
	using System.Reflection;
	using Attributes;

	public class ScenarioRunner
	{
		private List<ScenarioStep> steps;
		public IDictionary<Type, Func<string, object>> Parsers;
		private Type scenarioType;
		private string scenario;

		public ScenarioRunner()
		{
			Parsers = new Dictionary<Type, Func<string, object>>
			          {
			          	{ typeof( int ), input => int.Parse( input ) },
			          	{ typeof( float ), input => float.Parse( input ) },
			          	{ typeof( char ), input => input[ 0 ] },
			          	{ typeof( string ), input => input },
			          	{ typeof( DateTime ), input => DateTime.Parse( input ) },
			          	{ typeof( double ), input => double.Parse( input ) },
			          	{ typeof( Uri ), input => new Uri( input ) },
			          	{ typeof( decimal ), input => Decimal.Parse( input ) },
			          	{ typeof( byte ), input => byte.Parse( input ) },
			          	{ typeof( sbyte ), input => sbyte.Parse( input ) },
			          	{ typeof( bool ), input => bool.Parse( input ) }
			          };
		}

		public ScenarioRunner Load( string filename )
		{
			string[] lines = File.ReadAllLines( filename );

			scenario = ( from line in lines
			             where line.Trim().StartsWith( "Scenario:" )
			             select line.Trim().Substring( 9 ).Trim() ).SingleOrDefault();

			steps = ( from line in lines
			          where line.Trim().StartsWith( "given", StringComparison.CurrentCultureIgnoreCase ) ||
			                line.Trim().StartsWith( "when", StringComparison.CurrentCultureIgnoreCase ) ||
			                line.Trim().StartsWith( "then", StringComparison.CurrentCultureIgnoreCase ) ||
			                line.Trim().StartsWith( "and", StringComparison.CurrentCultureIgnoreCase )
			          select new ScenarioStep { Text = line, Result = Result.NotRun } ).ToList();

			return this;
		}

		public ReadOnlyCollection<ScenarioStep> Steps
		{
			get { return steps.AsReadOnly(); }
		}

		public string Scenario
		{
			get { return scenario; }
		}

		public bool Passed
		{
			get { return steps.TrueForAll( step => step.Result == Result.Passed ); }
		}

		public ScenarioRunner Parse<T>()
		{
			scenarioType = typeof( T );

			List<MethodInfo> methods = ( from method in typeof( T ).GetMethods( BindingFlags.Instance | BindingFlags.Public )
			                             where method.ReturnType == typeof( void ) && method.HasA<SpecificationBase>()
			                             select method ).ToList();

			foreach ( MethodInfo method in methods )
			{
				ScenarioStep matchingStep = steps.Find( step => method.Get<SpecificationBase>().Matches( step ) );

				if ( matchingStep != null )
				{
					matchingStep.Method = method;
					matchingStep.Pattern = method.Get<SpecificationBase>().Pattern;

					foreach ( ParameterInfo parameter in method.GetParameters() )
					{
						matchingStep.Parameters.Add( BuildMethodParameter( method, matchingStep, parameter ) );
					}
				}
			}

			return this;
		}

		private MethodParameter BuildMethodParameter( MethodInfo method, IScenarioStep step, ParameterInfo parameter )
		{
			var methodParameter = new MethodParameter( parameter.Name, parameter.ParameterType );

			if ( parameter.ParameterType.IsArray )
			{
				methodParameter.Value = BuildArrayFrom( parameter.ParameterType.GetElementType(),
				                                        method.Get<SpecificationBase>().ValueFor( parameter.Name, step.Text ) );
			}
			else
			{
				methodParameter.Value = ParserFor( parameter.ParameterType )( method.Get<SpecificationBase>().ValueFor( parameter.Name, step.Text ) );
			}

			return methodParameter;
		}

		private Func<string, object> ParserFor( Type type )
		{
			if ( Parsers.ContainsKey( type ) )
			{
				return Parsers[ type ];
			}

			throw new NactException( string.Format( "No parser for type {0}", type.Name ) );
		}

		private object BuildArrayFrom( Type elementType, string values )
		{
			string[] strings = values.Split( new[] { ',' } );

			Array instance = Array.CreateInstance( elementType, strings.Length );
			for ( int i = 0; i < strings.Length; i++ )
			{
				instance.SetValue( ParserFor( elementType )( strings[ i ] ), i );
			}

			return instance;
		}

		public void Run()
		{
			object scenarioInstance = scenarioType.GetConstructors()[ 0 ].Invoke( null );

			foreach ( ScenarioStep step in Steps )
			{
				object[] parameters = ( from parameter in step.Parameters select parameter.Value ).ToArray();

				try
				{
					if ( step.Method == null )
					{
						step.Result = Result.Ignored;
					}
					else
					{
						step.Method.Invoke( scenarioInstance, step.Parameters.Count > 0 ? parameters : null );
						step.Result = Result.Passed;
					}
				}
				catch ( Exception ex )
				{
					step.Result = Result.Failed;
					step.Exception = ex.InnerException;
				}
			}
		}
	}
}